package security;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;

import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.util.encoders.Base64;

import util.KeyReader;

/**
 * Provides a channel with authentication functionality.
 * After authentication the communication is AES-encoded.
 * 
 * @author Lukas
 *
 */
public class ClientAuthenticationChannel extends ChannelDecorator {
	private Channel secureChannel = null;
	private Channel unsecureChannel;
	private boolean waitForEndList, stopSendMethodWaiting = false;

	/**
	 * Constructor.
	 * @param channel The underlying channel that shall be used for unsecure transmission.
	 * @throws IOException
	 */
	public ClientAuthenticationChannel(Channel channel) {
		unsecureChannel = channel;
		this.channel = null;
	}

	/**
	 * Checks if the message contains the '!login' command; if so,
	 * the authentication is started.
	 * @throws IOException
	 */
	@Override
	public void send(String message) throws IOException {
		if (message.startsWith("!login ")) {
			loginProcess(message, false);
		} else if (message.startsWith("!autologin ")) {
			loginProcess(message.replaceFirst("auto", ""), true);
		} else {
			if (channel == null) {
				waitForEndList = message.equals("!list");
				channel = unsecureChannel;
				channel.send(message);
				while (channel != null && !stopSendMethodWaiting) { //before login: synchronized communication
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						throw new IOException(e);
					}
				}
			} else {
				channel.send(message);
			}
		}
	}
	
	@Override
	public String receive() throws IOException {
		while (channel == null) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				throw new IOException(e);
			}
		}
		
		String message;
		try {
			message = channel.receive();
		} catch (IOException e) {
			stopSendMethodWaiting = true;
			throw e;
		}
		if (message.equals("!unsecure")) {
			channel = null;
			message = unsecureChannel.receive();
		}
		if (message.equals("!lastSecure")) {
			message = channel.receive();
			channel = null;
		}
		if (channel == unsecureChannel && (message.equals("!endlist") || !waitForEndList)) {
			channel = null;
		}
		
		return message;
	}

	private void loginProcess(String message, boolean useSavedPwd) throws IOException {
		String user = message.split("\\s+")[1];
		
		try {
			SecureRandom rand = new SecureRandom();
			final byte[] clientChallenge = new byte[32];
			rand.nextBytes(clientChallenge);
			message += ' ' + new String(Base64.encode(clientChallenge));
			
			PublicKey pubKey;
			PrivateKey priKey;
			
			try {
				KeyReader kr = KeyReader.getInstance();
				pubKey = kr.readPublicKey("");
				//kr.setUserName(user);
				priKey = kr.readPrivateKey(useSavedPwd, user);
			} catch (FileNotFoundException e) {
				System.out.println(user + " is not a valid username");
				return;
			} catch (IOException e) {
				System.out.println("could not load private key");
				return;
			}
			
			RSAChannel rsaChannel = new RSAChannel(unsecureChannel, pubKey, priKey);
			rsaChannel.send(message);
			
			String[] response = rsaChannel.receive().split("\\s+");
			if (!response[0].equals("!ok")) {
				throw new SecurityException();
			}
			byte[] challengeResponse = Base64.decode(response[1].getBytes());
			if (challengeResponse.length != clientChallenge.length) {
				throw new SecurityException();
			}
			for (int i = 0; i < challengeResponse.length; i++) {
				if (challengeResponse[i] != clientChallenge[i]) {
					throw new SecurityException();
				}
			}
			SecretKey secKey = new SecretKeySpec(Base64.decode(response[3]), "AES");
			AlgorithmParameterSpec ivPar = new IvParameterSpec(Base64.decode(response[4]));
			
			secureChannel = new AESChannel(unsecureChannel, secKey, ivPar);
			channel = secureChannel;
			secureChannel.send(response[2]);
		} catch (SecurityException e) {
			channel = unsecureChannel;
			channel.send("!unsecure");
		}
	}
	
	@Override
	public void close() throws IOException {
		channel = unsecureChannel;
		channel.close();
	}
}
